#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""项目依赖扫描与自动打包工具
这个工具可以扫描Python项目中的依赖，并自动生成打包脚本。
使用方法：python dependency_scanner.py [--dir 目录路径] [--output 报告文件名]
"""
import sys
import os
import importlib
import traceback
import pkgutil
import argparse

class DependencyScanner:
    """依赖扫描器类，用于扫描项目中的Python依赖并生成相关报告"""
    def __init__(self, root_dir):
        """初始化扫描器
        Args:
            root_dir: 要扫描的根目录路径
        """
        self.root_dir = root_dir
        self.python_files = []
        self.imported_libraries = set()
        self.local_modules = set()
        self.import_results = {}
        self.main_file = None

    def find_python_files(self):
        """查找所有Python文件，并尝试识别主程序入口文件"""
        print(f"正在扫描目录: {self.root_dir}")
        for root, _, files in os.walk(self.root_dir):
            # 跳过一些不需要扫描的目录
            if any(excluded in root for excluded in ['__pycache__', 'build', 'dist', '.git', 'venv']):
                continue
            
            for file in files:
                if file.endswith('.py'):
                    file_path = os.path.join(root, file)
                    self.python_files.append(file_path)
                     
                    # 尝试识别主程序入口文件
                    if not self.main_file:
                        # 优先识别常见的主文件名
                        if file.lower() in ['main.py', 'app.py', 'run.py', '__main__.py']:
                            self.main_file = file_path
                        # 或者包含if __name__ == "__main__"的文件
                        try:
                            with open(file_path, 'r', encoding='utf-8') as f:
                                content = f.read()
                                if 'if __name__ == "__main__"' in content:
                                    self.main_file = file_path
                        except:
                            pass
        
        # 如果当前目录有main.py，优先使用它作为主程序
        main_py_path = os.path.join(self.root_dir, 'main.py')
        if os.path.exists(main_py_path) and main_py_path in self.python_files:
            self.main_file = main_py_path
        
        if self.main_file:
            print(f"找到主程序入口文件: {os.path.basename(self.main_file)}")
        else:
            print("未自动识别到主程序入口文件，可能需要手动指定")
            
        print(f"找到 {len(self.python_files)} 个Python文件")
        return self.python_files

    def extract_imports(self):
        """从Python文件中提取导入语句"""
        import_patterns = [
            'import ',
            'from ',
        ]
        
        for file_path in self.python_files:
            try:
                with open(file_path, 'r', encoding='utf-8') as f:
                    content = f.read()
                    
                # 获取文件名（不含扩展名）作为本地模块名
                file_name = os.path.splitext(os.path.basename(file_path))[0]
                self.local_modules.add(file_name)
                
                for line in content.split('\n'):
                    line = line.strip()
                    # 跳过注释和空行
                    if line.startswith('#') or not line:
                        continue
                    
                    # 处理import语句
                    for pattern in import_patterns:
                        if line.startswith(pattern):
                            # 移除可能的注释
                            if '#' in line:
                                line = line.split('#')[0].strip()
                            
                            # 解析导入的模块名
                            if pattern == 'import ':
                                # 处理 import module1, module2 as m2 的情况
                                parts = line[len(pattern):].split(',')
                                for part in parts:
                                    part = part.strip()
                                    if ' as ' in part:
                                        module = part.split(' as ')[0].strip()
                                    else:
                                        module = part
                                    
                                    # 只取顶级模块名
                                    module = module.split('.')[0]
                                    self.imported_libraries.add(module)
                            
                            elif pattern == 'from ':
                                # 处理 from module import something 的情况
                                if ' import ' in line:
                                    module_part = line[len(pattern):line.index(' import ')].strip()
                                    # 只取顶级模块名
                                    module = module_part.split('.')[0]
                                    self.imported_libraries.add(module)
            except Exception as e:
                print(f"解析文件 {file_path} 时出错: {e}")
        
        # 移除本地模块和标准库（基本判断）
        standard_libs = {'os', 'sys', 'math', 'time', 'datetime', 'collections', 're', 'json', 'csv'}
        third_party_libs = self.imported_libraries - self.local_modules - standard_libs
        
        print(f"发现 {len(self.imported_libraries)} 个导入的模块")
        print(f"其中 {len(third_party_libs)} 个可能是第三方库")
        return third_party_libs

    def test_imports(self, libraries):
        """测试导入第三方库"""
        print("\n开始测试导入第三方库...")
        
        for lib in sorted(libraries):
            try:
                # 尝试导入模块
                module = importlib.import_module(lib)
                
                # 获取模块信息
                module_path = getattr(module, '__file__', '未知路径')
                module_version = getattr(module, '__version__', '未知版本')
                
                self.import_results[lib] = {
                    'success': True,
                    'path': module_path,
                    'version': module_version
                }
                
                print(f"✅ {lib} (版本: {module_version}, 路径: {module_path})")
            except ImportError as e:
                self.import_results[lib] = {
                    'success': False,
                    'error': str(e)
                }
                print(f"❌ {lib}: 导入失败 - {e}")
            except Exception as e:
                self.import_results[lib] = {
                    'success': False,
                    'error': str(e),
                    'traceback': traceback.format_exc()
                }
                print(f"❌ {lib}: 发生错误 - {e}")
        
    def generate_report(self, output_file='dependency_report.txt'):
        """生成依赖分析报告"""
        print(f"\n生成依赖分析报告: {output_file}")
        
        with open(output_file, 'w', encoding='utf-8') as f:
            f.write("# 项目依赖分析报告\n")
            f.write("\n本报告由依赖扫描工具自动生成，包含项目依赖情况和打包建议。\n")
            f.write(f"\n## 扫描概览\n")
            f.write(f"- 扫描目录: {self.root_dir}\n")
            f.write(f"- 扫描时间: {self._get_current_time()}\n")
            f.write(f"- 扫描文件数: {len(self.python_files)}\n")
            f.write(f"- 发现导入模块数: {len(self.imported_libraries)}\n")
            
            if self.main_file:
                f.write(f"- 主程序入口: {os.path.basename(self.main_file)}\n")
            
            f.write(f"\n## 本地模块\n")
            for mod in sorted(self.local_modules):
                f.write(f"- {mod}\n")
            
            f.write(f"\n## 第三方库测试结果\n")
            success_count = sum(1 for info in self.import_results.values() if info['success'])
            f.write(f"- 成功导入: {success_count}/{len(self.import_results)}\n")
            
            f.write(f"\n### 成功导入的库\n")
            for lib, info in sorted(self.import_results.items()):
                if info['success']:
                    f.write(f"- **{lib}** (版本: {info['version']}, 路径: {info['path']})\n")
            
            f.write(f"\n### 导入失败的库\n")
            has_failures = False
            for lib, info in sorted(self.import_results.items()):
                if not info['success']:
                    has_failures = True
                    f.write(f"- **{lib}**: {info['error']}\n")
            
            if not has_failures:
                f.write("- 所有库都成功导入\n")
            
            f.write(f"\n## PyInstaller 配置建议\n")
            f.write(f"基于分析结果，建议在PyInstaller打包时添加以下隐藏导入:\n")
            f.write(f"```\n")
            
            # 为成功导入的库生成hidden-import参数
            hidden_imports = []
            for lib, info in sorted(self.import_results.items()):
                if info['success'] and lib != 'importlib':
                    hidden_imports.append(f"--hidden-import={lib}")
                    # 特殊处理一些常见的需要子模块的库
                    if lib == 'PyQt5':
                        hidden_imports.extend(['--hidden-import=PyQt5.QtWidgets', '--hidden-import=PyQt5.QtGui', '--hidden-import=PyQt5.QtCore'])
                    elif lib == 'matplotlib':
                        hidden_imports.extend(['--hidden-import=matplotlib.backends.backend_qt5agg'])
                    elif lib == 'numpy':
                        hidden_imports.extend(['--hidden-import=numpy.core._dtype_ctypes', '--hidden-import=numpy.core._multiarray_umath', '--hidden-import=numpy.lib.format'])
                    elif lib == 'pandas':
                        hidden_imports.extend(['--hidden-import=pandas._libs.tslibs.timedeltas', '--hidden-import=pandas._libs.tslibs.timestamps', '--hidden-import=pandas._libs.tslibs.base', '--hidden-import=pandas._libs.tslibs.np_datetime', '--hidden-import=pandas._libs.tslibs.parsing', '--hidden-import=pandas._libs.six'])
                    elif lib == 'scipy':
                        hidden_imports.extend(['--hidden-import=scipy.special.cython_special', '--hidden-import=scipy.spatial.ckdtree'])
            
            for imp in hidden_imports:
                f.write(f"{imp} \\\n")
            f.write(f"```\n")
            
            # 添加打包命令示例
            f.write(f"\n## 打包命令示例\n")
            if self.main_file:
                main_file_name = os.path.basename(self.main_file)
                app_name = os.path.splitext(main_file_name)[0].capitalize()
                
                f.write(f"### 基础打包命令\n")
                f.write(f"```bash\n")
                f.write(f"pyinstaller --onefile --name '{app_name}' {main_file_name}\n")
                f.write(f"```\n\n")
                
                f.write(f"### 增强版打包命令（含所有依赖）\n")
                f.write(f"```bash\n")
                f.write(f"pyinstaller --onefile --name '{app_name}' \\\n")
                for imp in hidden_imports[:5]:  # 只显示部分，避免报告过长
                    f.write(f"{imp} \\\n")
                if len(hidden_imports) > 5:
                    f.write("... # 更多hidden-import参数\n")
                f.write(f"{main_file_name}\n")
                f.write(f"```\n")
            else:
                f.write(f"请手动指定主程序入口文件进行打包。\n")
            
        print(f"报告已保存到: {os.path.abspath(output_file)}")
        
    
    def _get_current_time(self):
        """获取当前时间的格式化字符串"""
        import datetime
        return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        
    def generate_spec_file(self):
        """生成PyInstaller spec文件"""
        if not self.main_file:
            print("未找到主程序入口文件，无法自动生成spec文件。")
            return
        
        spec_path = os.path.join(self.root_dir, 'main.spec')
        main_file_name = os.path.basename(self.main_file)
        app_name = os.path.splitext(main_file_name)[0]
        
        # 构建hidden-imports列表
        hidden_imports = []
        for lib, info in sorted(self.import_results.items()):
            if info['success'] and lib != 'importlib':
                hidden_imports.append(lib)
                # 特殊处理一些常见的需要子模块的库
                if lib == 'PyQt5':
                    hidden_imports.extend(['PyQt5.QtWidgets', 'PyQt5.QtGui', 'PyQt5.QtCore'])
                elif lib == 'matplotlib':
                    hidden_imports.extend(['matplotlib.backends.backend_qt5agg'])
                elif lib == 'numpy':
                    hidden_imports.extend(['numpy.core._dtype_ctypes', 'numpy.core._multiarray_umath', 'numpy.lib.format'])
                elif lib == 'pandas':
                    hidden_imports.extend(['pandas._libs.tslibs.timedeltas', 'pandas._libs.tslibs.timestamps', 'pandas._libs.tslibs.base', 'pandas._libs.tslibs.np_datetime', 'pandas._libs.tslibs.parsing', 'pandas._libs.six'])
                elif lib == 'scipy':
                    hidden_imports.extend(['scipy.special.cython_special', 'scipy.spatial.ckdtree'])
        
        # 生成spec文件内容
        spec_content = f'''# -*- mode: python ; coding: utf-8 -*-

# 自动生成的PyInstaller spec文件
# 由依赖扫描工具生成，包含所有检测到的依赖项

block_cipher = None

a = Analysis(
    [r'{main_file_name}'],
    pathex=[r'{self.root_dir}'],
    binaries=[],
    datas=[],
    hiddenimports={hidden_imports},
    hookspath=[],
    hooksconfig={{}},
    runtime_hooks=[],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name='{app_name}',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=True,  # 显示控制台窗口
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)
'''
        
        with open(spec_path, 'w', encoding='utf-8') as f:
            f.write(spec_content)
            
        print(f"自动生成的spec文件已保存: {os.path.abspath(spec_path)}")
        print(f"使用说明：")
        print(f"1. 运行 'pyinstaller auto_package.spec' 进行打包")
        print(f"2. 打包结果将生成在 dist 目录下")
        print(f"3. 如需修改配置，可直接编辑此spec文件")

def main():
    """主函数，处理命令行参数并执行依赖扫描"""
    # 创建命令行参数解析器
    parser = argparse.ArgumentParser(description='项目依赖扫描与自动打包工具')
    parser.add_argument('--dir', type=str, default=os.path.dirname(os.path.abspath(__file__)),
                        help='要扫描的根目录（默认为当前脚本所在目录）')
    parser.add_argument('--output', type=str, default='dependency_report.txt',
                        help='输出报告文件名（默认为dependency_report.txt）')
    
    # 解析命令行参数
    args = parser.parse_args()
    
    print(f"========== 项目依赖扫描与自动打包工具 ==========")
    print(f"此工具可以扫描项目依赖并自动生成spec文件")
    print(f"=============================================")
    
    # 创建依赖扫描器实例
    scanner = DependencyScanner(args.dir)
    
    try:
        # 执行扫描和分析
        scanner.find_python_files()
        third_party_libs = scanner.extract_imports()
        scanner.test_imports(third_party_libs)
        scanner.generate_report(args.output)
        scanner.generate_spec_file()
        
        print("\n依赖扫描完成！")
        print(f"\n使用说明：")
        print(f"1. 查看生成的 {args.output} 了解详细依赖情况")
        print(f"2. 运行 'pyinstaller auto_package.spec' 进行打包")
        print(f"3. 打包结果将生成在 dist 目录下")
        print(f"4. 此工具是通用的，可复制到其他Python项目中使用")
    except Exception as e:
        print(f"\n扫描过程中发生错误: {e}")
        traceback.print_exc()
        sys.exit(1)

if __name__ == "__main__":
    main()